package jp.co.nes.awssupport.business.logic;

import java.math.BigDecimal;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;

import jp.co.nes.awssupport.access.dao.ReservedManageDao;
import jp.co.nes.awssupport.business.bean.BillingCsvBean;
import jp.co.nes.awssupport.business.constant.AwsProductName;
import jp.co.nes.awssupport.business.logic.description.Description;
import jp.co.nes.awssupport.business.logic.description.DescriptionFactory;
import jp.co.nes.awssupport.business.utility.AwsProductNameUtil;
import jp.co.nes.awssupport.business.utility.BillingCsvUtil;
import jp.co.nes.awssupport.business.utility.RateHelper;
import jp.co.nes.awssupport.business.utility.UsageTypeUtil;
import jp.co.nes.awssupport.business.utility.VolumingPlanHelper;
import jp.co.nes.awssupport.common.base.CommonException;
import jp.co.nes.awssupport.common.bean.ContractInformationBean;
import jp.co.nes.awssupport.common.bean.ReservedManageBean;
import jp.co.nes.awssupport.common.utility.BeanUtil;

/**
 * 請求CSVコンバータです。
 */
public class BillingCsvConvertor {

    /** リザーブド管理マップ(キー=リザーブドインスタンスID、値=リザーブド管理) */
    private Map<String, ReservedManageBean> mapReservedManage;

    /** 契約情報管理 */
    private ContractInformationManage ciManage;

    /** ボリューミングヘルパー */
    private VolumingPlanHelper volumingPlanHelper;

    /** 説明作成ファクトリ */
    private DescriptionFactory descriptionFactory;

    /** ワーニングメッセージリスト */
    private List<String> warnMessageList;

    /**
     * 処理対象年月を引数にもつコンストラクタです。
     *
     * @param targetYearMonth 処理対象年月
     */
    public BillingCsvConvertor(YearMonth targetYearMonth) {
        ReservedManageDao dao = new ReservedManageDao();
        mapReservedManage = dao.getMap(targetYearMonth);
        ciManage = new ContractInformationManage(targetYearMonth);
        volumingPlanHelper = new VolumingPlanHelper();
        descriptionFactory = new DescriptionFactory();
        warnMessageList = new ArrayList<>();
    }

    /**
     * 請求CSVに設定されている製品名、使用タイプ、使用量に関する変換処理と請求CSVへのレート、説明の設定処理を実施し、
     * 請求CSVを返却します。<br>
     * 集計処理に含めない請求CSVの場合、変換処理、設定処理をせず、nullを返却します。
     *
     * @param csvBean 変換する請求CSV
     * @return 請求CSV
     */
    public BillingCsvBean convert(BillingCsvBean csvBean) {
        BillingCsvBean dest = convertUsageType(csvBean);
        if (dest != null) {
            dest = convertProductName(dest);
            dest = settingRateAndUsageQuantity(dest);
            dest = settingDescription(dest);
        }
        return dest;
    }

    /**
     * 請求CSVの使用タイプを変換し、返却します。<br>
     * 集計処理に含めない請求CSVの場合、変換処理をせず、nullを返却します。
     *
     * @param csvBean 変換する請求CSV
     * @return 請求CSV
     */
    public BillingCsvBean convertUsageType(BillingCsvBean csvBean) {
        BillingCsvBean dest = new BillingCsvBean();
        BeanUtil.copyProperties(dest, csvBean);
        if (BillingCsvUtil.isReservedUpfront(dest)) {
            // リザーブド購入(前払い料金)の場合
            ReservedManageBean rmBean = getReservedManageBean(dest);
            if (checkNoUpfront(rmBean.getOfferingType())) {
                // No Upfrontは変換処理を実施せず、nullを返却する。
                return null;
            }

            dest.setUsageType(UsageTypeUtil.getForReservedUpfront(rmBean));
        } else if (BillingCsvUtil.isReservedMonthlyFixed(dest)) {
            // リザーブド定額月額料金の場合
            ReservedManageBean rmBean = getReservedManageBean(dest);
            if (checkNoUpfront(rmBean.getOfferingType())) {
                // 前払い無しの場合
                if (AwsProductNameUtil.isEquals(AwsProductName.EC2, csvBean.getProductName())) {
                    // リザーブド定額月額料金(前払い無し)(EC2)の場合
                    dest.setUsageType(UsageTypeUtil.getForReservedMonthlyFixedNoUpfrontEc2(dest, rmBean));
                } else {
                    // リザーブド定額月額料金(前払い無し)(EC2以外)の場合
                    dest.setUsageType(UsageTypeUtil.getForReservedMonthlyFixedNoUpfront(rmBean));
                }
            } else {
                // リザーブド定額月額料金(前払い無し以外)
                if (checkAllUpfront(rmBean.getOfferingType())) {
                    // All Upfrontは変換処理を実施せず、nullを返却する。
                    return null;
                }

                dest.setUsageType(UsageTypeUtil.getForReservedMonthlyFixed(dest, rmBean));
            }
        } else if (BillingCsvUtil.isReservedMetered(dest)) {
            // リザーブドインスタンスの従量料金の場合
            if (BillingCsvUtil.isBlendedReserved(dest)) {
                // 融通リザーブドの場合
                if (AwsProductNameUtil.isEquals(AwsProductName.EC2, csvBean.getProductName())) {
                    dest.setUsageType(UsageTypeUtil.getForBlendedReservedEc2(dest));
                }
            } else {
                // 融通リザーブドでない場合
                ReservedManageBean rmBean = getReservedManageBean(dest);
                if (checkNoUpfront(rmBean.getOfferingType())) {
                    // 前払い無しの場合
                    if (AwsProductNameUtil.isEquals(AwsProductName.EC2, csvBean.getProductName())) {
                        // リザーブド(前払い無し)(EC2)の場合
                        dest.setUsageType(UsageTypeUtil.getForReservedMeteredNoUpfrontEc2(dest, rmBean));
                    } else {
                        // リザーブド(前払い無し)(EC2以外)の場合
                        dest.setUsageType(UsageTypeUtil.getForReservedMeteredNoUpfront(rmBean));
                    }
                } else {
                    // リザーブド(前払い無し以外)の場合
                    dest.setUsageType(UsageTypeUtil.getForReservedMetered(dest, rmBean));
                }
            }
        }

        String usageType = dest.getUsageType();
        String productName = dest.getProductName();
        String operation = dest.getOperation();

        usageType = UsageTypeUtil.missingCompletion(usageType, productName, operation);
        usageType = UsageTypeUtil.operationCompletion(usageType, productName, operation);
        usageType = UsageTypeUtil.changeRegionNameToRegionCode(usageType, productName);
        usageType = UsageTypeUtil.changeRegionCodeToAnotherRegion(usageType);

        dest.setUsageType(usageType);
        return dest;
    }

    /**
     * 変換処理を実施中に発生したワーニングメッセージリストを返却します。
     *
     * @return ワーニングメッセージリスト
     */
    public List<String> getWarnMessageList() {
        return warnMessageList;
    }

    /**
     * 請求CSVに設定されている連結アカウントIDに対応する契約情報を返却します。
     *
     * @param csvBean 請求CSV
     * @return 契約情報
     */
    protected ContractInformationBean getContractInformationBean(BillingCsvBean csvBean) {
        return ciManage.get(csvBean.getLinkedAccountId());
    }

    /**
     * 請求CSVの製品名を変換し、返却します。
     *
     * @param csvBean 変換する請求CSV
     * @return 請求CSV
     */
    private BillingCsvBean convertProductName(BillingCsvBean csvBean) {
        BillingCsvBean dest = new BillingCsvBean();
        BeanUtil.copyProperties(dest, csvBean);
        if (volumingPlanHelper.isApply(dest.getProductName(), dest.getUsageType())) {
            // ボリューミングプランを適用する場合
            if (!AwsProductNameUtil.isEquals(AwsProductName.CLOUD_FRONT, dest.getProductName())
                    && !AwsProductNameUtil.isEquals(AwsProductName.ELASTIC_SEARCH_SERVICE, dest.getProductName())) {
                if (dest.getUsageType().endsWith(VolumingPlanHelper.MERGE_USAGE_TYPE_SUFFIX)) {
                    // 使用タイプが、<リージョンコード>-DataTransfer-Out-BytesのものはProdctNameを変更する。
                    dest.setProductName(VolumingPlanHelper.MERGE_PRODUCT_NAME);
                }
            }
        }
        return dest;
    }

    /**
     * 請求CSVのレートを設定し(必要に応じて使用量を変換が必要だった場合は使用量も変換する)、返却します。
     *
     * @param csvBean レートを設定する請求CSV
     * @return 請求CSV
     */
    private BillingCsvBean settingRateAndUsageQuantity(BillingCsvBean csvBean) {
        BillingCsvBean dest = new BillingCsvBean();
        BeanUtil.copyProperties(dest, csvBean);

        RateHelper rateHelper = new RateHelper();
        if (BillingCsvUtil.isSpot(dest)) {
            // スポットの場合
            rateHelper.calcForSpot(dest, getContractInformationBean(dest));
        } else if (BillingCsvUtil.isReservedUpfront(dest)) {
            // リザーブド購入(前払い料金)の場合
            rateHelper.calcForReservedUpfront(dest, getContractInformationBean(dest));
        } else if (BillingCsvUtil.isReservedMonthlyFixed(dest)) {
            // リザーブド定額月額料金の場合
            rateHelper.calcForReservedMonthlyFixed(dest, getContractInformationBean(dest));
        } else if (BillingCsvUtil.isReservedMetered(dest) && BillingCsvUtil.isBlendedReserved(dest)) {
            // リザーブドインスタンスの従量料金の場合 and 融通リザーブドの場合
            rateHelper.calcForBlendedReserved(dest, getContractInformationBean(dest));
        } else if (BillingCsvUtil.isReservedMetered(dest) && !BillingCsvUtil.isBlendedReserved(dest)) {
            // リザーブドインスタンスの従量料金の場合 and 融通リザーブドでない場合
            rateHelper.calcForReservedMetered(dest, getContractInformationBean(dest));
        } else {
            // オンデマンドの場合
            rateHelper.calcForOnDemand(dest, getContractInformationBean(dest));
        }

        BigDecimal rate = rateHelper.getRate();
        BigDecimal usageQuantity = rateHelper.getUsageQuantity();
        dest.setRate(rate);
        if (usageQuantity != null) {
            // レート設定に伴い、使用量の変更も必要だった場合
            dest.setUsageQuantity(usageQuantity);
        }
        return dest;
    }

    /**
     * 請求CSVの説明を設定し(必要に応じて使用量を変換が必要だった場合は使用量も変換する)、返却します。
     *
     * @param csvBean 説明を設定する請求CSV
     * @return 請求CSV
     */
    private BillingCsvBean settingDescription(BillingCsvBean csvBean) {
        BillingCsvBean dest = new BillingCsvBean();
        BeanUtil.copyProperties(dest, csvBean);

        Description description = descriptionFactory.getInstance(dest);

        if (BillingCsvUtil.isSpot(dest)) {
            // スポットの場合
            dest.setDescription(description.getForSpot(dest));
        } else if (BillingCsvUtil.isReservedUpfront(dest)) {
            // リザーブド購入(前払い料金)の場合
            dest.setDescription(description.getForReservedUpfront(dest, getReservedManageBean(dest)));
        } else if (BillingCsvUtil.isReservedMonthlyFixed(dest)) {
            // リザーブド定額月額料金の場合
            dest.setDescription(description.getForReservedMonthlyFixed(dest, getReservedManageBean(dest)));
        } else if (BillingCsvUtil.isReservedMetered(dest) && BillingCsvUtil.isBlendedReserved(dest)) {
            // リザーブドインスタンスの従量料金の場合 and 融通リザーブドの場合
            dest.setDescription(description.getForBlendedReserved(dest));
        } else if (BillingCsvUtil.isReservedMetered(dest) && !BillingCsvUtil.isBlendedReserved(dest)) {
            // リザーブドインスタンスの従量料金の場合 and 融通リザーブドでない場合
            dest.setDescription(description.getForReservedMetered(dest, getReservedManageBean(dest)));
        } else {
            // オンデマンドの場合
            dest.setDescription(description.getForOnDemand(dest));
        }

        List<String> descriptionWarnMessageList = description.getWarnMessageList();
        for (String warnMessage : descriptionWarnMessageList) {
            if (!warnMessageList.stream().anyMatch(s -> s.equals(warnMessage))) {
                warnMessageList.add(warnMessage);
            }
        }

        return dest;
    }

    /**
     * リザーブド管理を返却します。
     *
     * @param csvBean 請求CSV
     * @return リザーブド管理
     */
    private ReservedManageBean getReservedManageBean(BillingCsvBean csvBean) {
        String productName = csvBean.getProductName();
        String reservationARN = csvBean.getReservationARN();
        String id = BillingCsvUtil.getIdFromReservationARN(productName, reservationARN);
        if (StringUtils.isEmpty(id)) {
            StringBuilder builder = new StringBuilder();
            builder.append("id is null or empty(linkedAccountId=");
            builder.append(csvBean.getLinkedAccountId());
            builder.append(",productName=");
            builder.append(productName);
            builder.append(",usageType=");
            builder.append(csvBean.getUsageType());
            builder.append(",reservationARN=");
            builder.append(reservationARN);
            builder.append(")");
            throw new CommonException(builder.toString());
        }
        ReservedManageBean rmBean = mapReservedManage.get(id);
        if (rmBean == null) {
            StringBuilder builder = new StringBuilder();
            builder.append("reserved manage not exist(linkedAccountId=");
            builder.append(csvBean.getLinkedAccountId());
            builder.append(",productName=");
            builder.append(productName);
            builder.append(",usageType=");
            builder.append(csvBean.getUsageType());
            builder.append(",id=");
            builder.append(id);
            builder.append(")");
            throw new CommonException(builder.toString());
        }
        return rmBean;
    }

    /**
     * 前払い無しかどうか確認します。
     *
     * @param offeringType 要求タイプ
     * @return true: 前払い無しである, false: 前払い無しでない
     */
    private boolean checkNoUpfront(String offeringType) {
        boolean isNoUpfront = false;
        if (!StringUtils.isEmpty(offeringType) && offeringType.equals("No Upfront")) {
            isNoUpfront = true;
        } else {
            if (!StringUtils.isEmpty(offeringType) && offeringType.toLowerCase().equals("no upfront")) {
                // "No Upfront"の判断の大文字、小文字は左記の内容だが、大文字、小文字に関する内容が原因で、
                // 判断がNGになっている場合、リザーブド管理に想定外の内容が設定されているので、例外を発生させる。
                throw new CommonException("OfferingType is wrong(offeringType=" + offeringType + ")");
            }
        }
        return isNoUpfront;
    }

    /**
     * 全額前払いかどうか確認します。
     *
     * @param offeringType 要求タイプ
     * @return true: 全額前払いである, false: 全額前払いでない
     */
    private boolean checkAllUpfront(String offeringType) {
        boolean isAllUpfront = false;
        if (!StringUtils.isEmpty(offeringType) && offeringType.equals("All Upfront")) {
            isAllUpfront = true;
        } else {
            if (!StringUtils.isEmpty(offeringType) && offeringType.toLowerCase().equals("all upfront")) {
                // "All Upfront"の判断の大文字、小文字は左記の内容だが、大文字、小文字に関する内容が原因で、
                // 判断がNGになっている場合、リザーブド管理に想定外の内容が設定されているので、例外を発生させる。
                throw new CommonException("OfferingType is wrong(offeringType=" + offeringType + ")");
            }
        }
        return isAllUpfront;
    }
}
